要在父子元件之間傳遞變數或內容的時候,我們可以使用 props,但如果想要傳遞進去的是一段 template 片段,就要使用到 slot。
最簡單明瞭的 slot 範例:
如上述範例, Vue 會在子元件的 slot 插入 textNode (#text),渲染出 <button>Click Me<button>
。
我剛開始看到的時候,其實蠻疑惑的,這樣跟透過 props
傳進去,似乎沒有太大的差異,所以 slot 到底要用在哪?
開始做案子之後,才發現 v-slot 的好用之處,可以讓元件變得更彈性,透過傳入 template 來客製部份元件,所以實際上用到 v-slot 的狀況,通常不會只傳入簡單的文字內容。
接下來會用兩個範例,來示範 v-slot 在 real world 開發中好用的地方。
過程中會講解到的 slot 相關語法:
要示範 v-slot 好用之處,我覺得最棒的案例就是表格!(展示型的網站比較少用到表格,但在開發後台系統的時候很常用到。)
大部分使用表格的狀況,只需要渲染一般資料內容,像這樣:
姓名 | 主題 | 備註 | |
---|---|---|---|
1 | Angela | 真的好想離開 Vue 3 新手村 | Composition API |
2 | 阿傑 | 咩色用得好,歸剛沒煩惱 | 從 ECMAScript 偷窺 JavaScript Array method |
3 | Jade | 前端蛇行撞牆記 | - |
4 | Vic | JavaScript 之路,往前邁進吧! | 未來會成為JS大師的人。 |
只要單純呈現資料的話,Table 元件可以很簡單,利用 v-for 渲染傳進來的標頭(Array
)和資料(Array[Object]
)就可以了,元件模板大概會像這樣:
<template>
<table>
<thead>
<th></th>
<th v-for="tableHead in props.tableHeads" :key="tableHead">
{{ tableHead }}
</th>
</thead>
<tbody>
<tr v-for="(item, itemIndex) in props.tableBody" :key="user.name">
<th>{{ itemIndex + 1 }}</th>
<td v-for="(value, key, index) in item" :key="value">
{{ value }}
</td>
</tr>
</tbody>
</table>
</template>
有時候表格會需要一些特殊欄位,例如 <a>
連結、按鈕(點擊時呼叫 method)、圖片或 icon 等等。
不同頁面的特殊欄位又會不一樣,想要做到每次引用元件時,都可以自由增加自訂欄位格式,就可以透過 v-slot 做到。
以剛剛的範例進行擴充,加入「查看」的連結,能點擊對應的鐵人發文頁面,預期呈現的畫面如下:
姓名 | 主題 | 備註 | 查看 | |
---|---|---|---|---|
1 | Angela | 真的好想離開 Vue 3 新手村 | Composition API | 查看 |
2 | 阿傑 | 咩色用得好,歸剛沒煩惱 | 從 ECMAScript 偷窺 JavaScript Array method | 查看 |
3 | Jade | 前端蛇行撞牆記 | - | 查看 |
4 | Vic | JavaScript 之路,往前邁進吧! | 未來會成為JS大師的人。 | 查看 |
資料形式如下:
const tableHeads = ref(["姓名", "主題", "備註", "查看"]);
const tableBody = ref([
{
name: "Angela",
topic: "真的好想離開 Vue 3 新手村",
note: "Composition API",
ithelpLink: "https://ithelp.ithome.com.tw/users/20152606/ironman/5782",
},
//後略
])
預期使用方式:
在使用元件時,傳入 name
為 ithelpLink
的 slot,Vue 會將這個 slot 內容塞到表格中 ithelpLink
/查看那一行的格子內,引用時看起來會像下面這樣:
<template>
<MyTable :tableHeads="tableHeads" :tableBody="tableBody">
<!-- 查看連結 -->
<template #ithelpLink>
~~客製化欄位~~
</template>
<template #其他項目的key>
~~客製化欄位~~
</template>
</MyTable>
</template>
接下來會分步驟處理,並講解語法。
大家可以用 fork 這份 code,跟著下面的解說一起練習。
v-slot
可以縮寫成 #
,後面接的參數為 slot 區塊的名稱(name
屬性),如果不給予名稱則預設為 default,但 default 只能有一個。
<template>
,透過 v-slot:slot名稱
或 #slot名稱"
宣告 slot 的名稱<slot>
區塊,給予 name
屬性,表示要插入的 slot 的名稱透過對 slot 區塊命名,指定傳入的內容要應用在哪裡。
在子元件內使用 v-for 迭代 tableBody
資料時,當資料(物件)的 key 等於 slot 名稱時,將 slot 內容嵌到 <td>
下。
<tbody>
<tr v-for="(item, itemIndex) in props.tableBody" :key="item.name">
<th>{{ itemIndex + 1 }}</th>
<td v-for="(value, key, index) in item" :key="value">
<span v-if="如果沒有傳入和 key 相同名稱的 slot">{{ value }}</span>
<slot v-else :name="key"></slot> <!--重點是這一行!-->
</td>
</tr>
</tbody>
承上,要如何判別父層傳入哪些 slot?
v-if="如果沒有傳入和 key 相同名稱的 slot"
useSlots
<script setup>
import { useSlots } from "vue";
const slots = useSlots();
</script>
$slot
$slots
是物件,key
為每個 slot 的名稱,我們可以透過 Object.keys()
將傳入的 slot 名稱迭代出來做判斷。<tbody>
<tr v-for="(item, itemIndex) in props.tableBody" :key="item.name">
<th>{{ itemIndex + 1 }}</th>
<td v-for="(value, key, index) in item" :key="value">
<span v-if="!Object.keys($slots).includes(key)">{{ value }}</span>
<slot v-else :name="key"></slot>
</td>
</tr>
</tbody>
因為 slot 內容是在父層定義的,所以可以直接拿到父層的資料,沒有辦法拿到元件層的資料,元件層的資料可以透過 props
傳入 <slot>
內。
以剛剛的範例來說:
tableHeads
和 tableBody
是從父層傳入元件的 props,這兩筆資料我們可以直接在元件層拿到;但我們在渲染表格的時候,需要知道透過 v-for 進行列表渲染時,該項 value
或 index
,才能取到對應的 ithelpLink
連結,而這些渲染資訊(value
或 index
)被放在子元件內。
<template>
<MyTable :tableHeads="tableHeads" :tableBody="tableBody">
<!-- 查看連結 -->
<template #ithelpLink>
<a :href="tableBody[index].ithelpLink" target="_blank">查看</a>
</template>
</MyTable>
</template>
所以要將子元件的資料傳入元件層的 <slot>
:
name
屬性是用來對應 slot 名稱value
、index
、甚至是元件收到的全部 props
<tbody>
<tr v-for="(item, itemIndex) in props.tableBody" :key="item.name">
<th>{{ itemIndex + 1 }}</th>
<td v-for="(value, key, index) in item" :key="value">
<span v-if="!Object.keys($slots).includes(key)">{{ value }}</span>
<slot v-else :name="key" :value="value" :index="itemIndex" :props="props"></slot>
</td>
</tr>
</tbody>
透過 v-bind 傳入的值,可以在父層從 v-slot 指令拿到。
所有傳給 <slot>
的屬性會被集合成物件,可以直接解構需要的屬性,讓模板變得更簡潔。
<MyTable :tableHeads="tableHeads" :tableBody="tableBody">
<!-- 查看連結 -->
<template #ithelpLink="{ value, index, props }">
<!-- 取得當筆 value -->
<a :href="value" target="_blank">查看</a>
<!-- 從父層拿 tableBody -->
<a :href="tableBody[index].ithelpLink" target="_blank">查看</a>
<!-- 從元件層的 props 拿 tableBody -->
<a :href="props.tableBody[index].ithelpLink" target="_blank">查看</a>
</template>
</MyTable>
以今天的範例來說,直接從 value
就可以取得文章連結,主要是為了示範 slot 可以拿到的資料,所以多傳其他屬性,多寫幾種取法。
畫重點:slot 的 scope 在父層,但透過 v-bind,可以同時拿到父層跟子層的資料做運用。
最後成果可以到這裡看。
透過運用 v-slot,就可以在使用表格元件時,自訂需要的欄位格式或樣式,要幾個加幾個、想長怎麼樣就長怎麼樣。
這也是為什麼 v-slot 可以讓元件更彈性,在不同的情況下依然能複用。
說真的,想看元件「彈性的極致」,那不就是 UI framework 嗎?他們的 table 元件都有預留 slot 欄位,提供開發者客製化欄位結構或樣式,以 Quasar 為例,他提供的 QTable 就有設計高達 19 個 slot 區塊。
以 BootstrapVue 的 Pagination 為例,在「第一頁」、「前一頁」、「分頁頁數」、「中間刪節」等處都留了 <slot>
,讓開發者可以傳進自訂內容和樣式。
當然在一般網站開發下,沒有必要在自訂元件上,預設所有可能用到的 slot 區塊(過度設計),但如果專案有搭配 UI framework 進行開發,多少會需要將自己客製的 template 片段,傳進 UI 框架幫你挖好的 slot 中,所以還是有必要了解 v-slot 的使用方式!
Vue 在 slot guide 篇章提了一個很有趣的使用概念 - Renderless Components。
我們很常在元件內封裝邏輯和畫面,利用 v-slot,可以將邏輯封裝在元件層,渲染的部份則一樣由父層負責,元件本身不需要負責渲染的工作。
官方文件的案例是一個 MouseTracker,可以在 Vue SFC Playground 玩玩看。
今天會用 Vue 的內建 <Transition>
元件作為說明案例。
<Transition>
先看 <Transition>
的使用方式與效果:
在 <template>
:
<button @click="show = !show">Toggle</button>
<Transition>
<p v-if="show">hello</p>
</Transition>
在 <style>
:
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
<Transition>
元件本身就有運用到 <slot>
的概念,他會將進入、離開的動畫應用 default slot 的內容上,也就是我們傳入 <Transition></Transition>
內的元素或元件上。
我們可以利用 <Transition>
加上自訂的動畫效果,做成可以複用的 Reusable Transitions,(這其實也是 Vue 在 <Transition>
章節提到的概念)。
SlideFadeTransition.vue
:
<Transition>
一個 name
屬性,要對應到 CSS style 的前綴。<Transition>
內。<template>
<Transition name="slide-fade">
<slot />
</Transition>
</template>
注意樣式 <style>
不可以加上 scoped
屬性,否則無法應用在父層的 <slot>
上。
.slide-fade-enter-active {
transition: all 0.3s ease-out;
}
.slide-fade-leave-active {
transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateX(20px);
opacity: 0;
}
更多可以調用的 CSS class name 可以 參考這邊
在 App.vue
引用:
<SlideFadeTransition
<p v-if="show">hello</p>
</SlideFadeTransition>
每次要用到 Slide-Fade Transition 的效果時,就不需要重寫 CSS 樣式,直接引用 <SlideFadeTransition>
即可。
我覺得 slot 真的要透過實作來練習,像共用表格元件就是一個不錯的練習!(不過實作類型的文章真的好難寫喔QQ)
自訂複用的 transition 元件在 <slot>
的應用上很單純,比較複雜的是在於了解有哪些屬性可以傳進 <Transition>
中,調整動畫的呈現,有興趣的人可以到這裡看文件。